package io.hepu.robotize.base;

import io.hepu.robotize.aop.PreCreate;
import io.hepu.robotize.aop.PreUpdate;
import io.hepu.robotize.page.PageRequest;
import io.hepu.robotize.page.PageResponse;
import io.hepu.robotize.util.Generics;
import io.hepu.robotize.util.Limits;
import io.hepu.robotize.util.MapKeyReplacer;
import io.hepu.robotize.util.Reflects;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;

import javax.annotation.Resource;
import javax.persistence.Column;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public abstract class BaseService<T extends BaseEntity<ID>, ID extends Serializable, R extends IBaseRepository<T, ID>> implements IBaseService<T, ID> {

    @Resource(name = "sqlSessionFactory")
    private SqlSessionFactory sqlSessionFactory;

    @Autowired(required = false)
    private R resposity;

    private final Class<?> entityClass;

    public BaseService() {
        entityClass = Generics.getTypeClass(getClass());
    }

    protected R getResposity() {
        return resposity;
    }

    public String getCacheKey(Object uniqueKey) {
        return getClass().getName() + ":" + uniqueKey;
    }

    @Override
    public ID findLastId(String table) {
        return resposity.selectLastId(table);
    }

    // Finds one entity form the database.
    @Override
    public T findOne(ID id) {
        return resposity.selectById(id);
    }

    @Override
    public T findOne(T entity) {
        return resposity.selectOne(entity);
    }

    // Inserts a given entity into the database.
    @PreCreate
    @Override
    public Integer saveOne(T entity) {
        return resposity.insertSelective(entity);
    }

    /**
     * Updates a given entity into the database.
     *
     * @return 1=success(even without making any changes), 0=failed
     */
    @PreUpdate
    @Override
    public Integer updateOne(T entity) {
        return resposity.updateSelective(entity);
    }

    // Deletes one entity from the database.
    @Override
    public Integer deleteOne(ID id) {
        return resposity.deleteById(id);
    }

    // Deletes one entity from the database.
    @Override
    public Integer deleteOne(T entity) {
        return resposity.deleteSelective(entity);
    }

    // Deletes entities with the specified keys.
    @Override
    public void delete(ID[] keys) {
        resposity.deleteSome(keys);
    }

    @PreCreate
    @Override
    public void insert(Collection<T> entities) {
        batch(entities, resposity::insertSelective);
    }

    @PreUpdate
    @Override
    public void update(Collection<T> entities) {
        batch(entities, resposity::updateSelective);
    }

    @Override
    public void delete(Collection<T> entities) {
        batch(entities, resposity::deleteSelective);
    }

    @Override
    public List<T> list(Map<String, Object> conditions) {
        // Resovle table name by annotation
        String table = Objects.requireNonNull(table(), "Table name cannot be NULL");
        return resposity.list(table, conditions);
    }

    @Override
    @Cacheable(value = "PAGING", key = "#root.target.getCacheKey(#request.hashCode())")
    public PageResponse<T> paginate(PageRequest request) {
        // Resovle table name by annotation
        String table = Objects.requireNonNull(table(), "Table name cannot be NULL");
        // If field, replace with column name
        resovleColumn(request);
        // Fetch the paginated data
        List<T> elements = resposity.paginate(table, request);
        // Calculate the total number of records matching the conditions
        long total = resposity.total(table, request.getConditions());
        return new PageResponse<>(elements, total, request.getPage(), request.getSize());
    }

    @Override
    public Integer saveOrUpdate(T entity) {
        T selectOne = findOne(entity.getId());
        return Limits.nonNull(selectOne) ? updateOne(entity) : saveOne(entity);
    }

    @Override
    public <S extends IBaseService<T, ID>> Integer saveOrUpdate(S service, T entity) {
        T selectOne = findOne(entity.getId());
        return Limits.nonNull(selectOne) ? service.updateOne(entity) : service.saveOne(entity);
    }

    @FunctionalInterface
    private interface BatchOperation<T> {
        void apply(T entity);
    }

    // Generic batch operation method to avoid code duplication
    private void batch(Collection<T> entities, BatchOperation<T> operation) {
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            entities.forEach(operation::apply);
            sqlSession.commit();
        }
    }

    private String table() {
        Table table = Reflects.getClassAnnotation(entityClass, Table.class);
        return null != table ? table.name() : null;
    }

    private String column(String fieldName) {
        Column column = Reflects.getFieldAnnotation(entityClass, fieldName, Column.class);
        return null != column ? column.name() : fieldName;
    }

    private void resovleColumn(PageRequest request) {
        MapKeyReplacer.FieldMapper<String> fieldMapper = this::column; // fieldName -> column(fieldName);
        MapKeyReplacer.replace(request.getConditions(), fieldMapper);
        MapKeyReplacer.replace(request.getOrders(), fieldMapper);
    }
}
